#!/usr/bin/python3

import json
import sys
import os
import pipes
import socket
from threading import Thread
import fcntl
import shutil

import traceback
import time
from contextlib import closing

import requests

import queue as queue
from queue import Queue, Empty

taskQ = Queue()
submission = None

# path related function

def uoj_url(uri):
	return ("%s://%s%s" % (jconf['uoj_protocol'], jconf['uoj_host'], uri)).rstrip('/')
def uoj_judger_path(path = ''):
	return "uoj_judger" + path

# os related funciton
def clean_up_folder(path):
	for f in os.listdir(path):
		f_path = os.path.join(path, f)
		if os.path.isfile(f_path):
			os.unlink(f_path)
		else:
			shutil.rmtree(f_path)

def execute(cmd):
	if os.system(cmd):
		raise Exception('failed to execute: %s' % cmd)

def freopen(f, g):
	os.dup2(g.fileno(), f.fileno())
	g.close()

# init
def init():
	global jconf
	os.chdir(os.path.dirname(os.path.realpath(__file__)))
	with open('.conf.json', 'r') as fp:
		jconf = json.load(fp)
		assert 'uoj_protocol' in jconf
		assert 'uoj_host' in jconf
		assert 'judger_name' in jconf
		assert 'judger_password' in jconf
		assert 'socket_port' in jconf
		assert 'socket_password' in jconf

# socket server
def socket_server_loop():
	SOCK_CLOEXEC = 524288
	with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM | SOCK_CLOEXEC)) as s:
		s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
		s.bind(('', jconf['socket_port']))
		s.listen(5)
		
		while True:
			try:
				conn, addr = s.accept()
				with closing(conn) as conn:
					data = conn.recv(1024).decode()
					assert data != None
					task = json.loads(data)
					assert task['password'] == jconf['socket_password']
					assert 'cmd' in task
					
					taskQ.put(task)
					
					if task['cmd'] == 'stop':
						print('the judge client is closing...')
						taskQ.join()
						conn.sendall(b'ok')
						return 'stop'
			except Exception:
				print('['+time.asctime()+']', 'connection rejected', file=sys.stderr)
				traceback.print_exc()
			else:
				print('['+time.asctime()+']', 'a new task accomplished', file=sys.stderr)

def start_judger_server():
	global socket_server_thread
	
	print_judge_client_status()
	print('hello!', file=sys.stderr)
	
	socket_server_thread = Thread(target = socket_server_loop)
	socket_server_thread.setDaemon(True)
	socket_server_thread.start()
	
	judger_loop()

# report thread
def report_loop():
	if 'is_hack' in submission:
		return
	while not submission_judged:
		try:
			with open(uoj_judger_path('/result/cur_status.txt'), 'r') as f:
				fcntl.flock(f, fcntl.LOCK_SH)
				try:
					status = f.read(100)
				except Exception:
					status = None
				finally:
					fcntl.flock(f, fcntl.LOCK_UN)
			
			if status != None:
				data = {}
				data['update-status'] = True
				data['id'] = submission['id']
				if 'is_custom_test' in submission:
					data['is_custom_test'] = True
				data['status'] = status
				uoj_interact(data)
			time.sleep(0.2)
		except Exception:
			pass

# handle task in main thread
def handle_task():
	need_restart = False
	try:
		while True:
			task = taskQ.get_nowait()
			
			if task['cmd'] == 'update':
				try:
					uoj_download('/judger', 'judger_update.zip')
					execute('unzip -o judger_update.zip && cd %s && make clean && make' % uoj_judger_path())
				except:
					print(sys.stderr, "error when update")
				if jconf['judger_name'] == 'main_judger':
					uoj_sync_judge_client()
				need_restart = True
			elif task['cmd'] == 'stop':
				taskQ.task_done()
				socket_server_thread.join()
				
				print_judge_client_status()
				print(sys.stderr, "goodbye!")
				
				sys.exit(0)
			
			taskQ.task_done()
	except Empty:
		pass
	
	if need_restart:
		os.execl('./judge_client', './judge_client')

def print_judge_client_status():
	print('[' + time.asctime() + ']', end=' ', file=sys.stderr)
	if submission != None:
		print(submission, end=' ', file=sys.stderr)
	print(file=sys.stderr)

# interact with uoj_judger
def get_judger_result():
	res = {}
	with open(uoj_judger_path('/result/result.txt'), 'r') as fres:
		res['score'] = 0
		res['time'] = 0
		res['memory'] = 0
		while True:
			line = fres.readline()
			if line == '':
				break
			line = line.strip()
			if line == 'details':
				res['details'] = fres.read()
				break
			
			sp = line.split()
			assert len(sp) >= 1
			if sp[0] == 'error':
				res['error'] = line[len('error') + 1:]
			else:
				assert len(sp) == 2
				res[sp[0]] = sp[1]
		res['score'] = int(res['score'])
		res['time'] = int(res['time'])
		res['memory'] = int(res['memory'])
	res['status'] = 'Judged'
	return res

def update_problem_data(problem_id, problem_mtime):
	try:
		if jconf['judger_name'] == 'main_judger':
			return
		copy_name = uoj_judger_path('/data/%d' % problem_id)
		copy_zip_name = uoj_judger_path('/data/%d.zip' % problem_id)
		if os.path.isdir(copy_name):
			quoted_copy_name = pipes.quote(copy_name)
			if os.path.getmtime(copy_name) >= problem_mtime:
				execute('touch -a %s' % quoted_copy_name)
				return
			else:
				execute('chmod 700 %s -R && rm -rf %s' % (quoted_copy_name, quoted_copy_name))
		del_list = sorted(os.listdir(uoj_judger_path('/data')), key=lambda fname: os.path.getatime(uoj_judger_path('/data/%s' % fname)))[:-99]
		for fname in del_list:
			quoted_fname = pipes.quote(uoj_judger_path('/data/%s' % fname))
			os.system('chmod 700 %s -R && rm -rf %s' % (quoted_fname, quoted_fname))
		
		uoj_download('/problem/%d' % problem_id, copy_zip_name)
		execute('cd %s && unzip -q %d.zip && rm %d.zip && chmod -w %d -R' % (uoj_judger_path('/data'), problem_id, problem_id, problem_id))
	except Exception:
		print_judge_client_status()
		traceback.print_exc()
		raise Exception('failed to update problem data of #%d' % problem_id)
	else:
		print_judge_client_status()
		print('updated problem data of #%d successfully' % problem_id, file=sys.stderr)

def judge():
	global report_thread
	global submission_judged
	
	clean_up_folder(uoj_judger_path('/work'))
	clean_up_folder(uoj_judger_path('/result'))
	update_problem_data(submission['problem_id'], submission['problem_mtime'])
	
	with open(uoj_judger_path('/work/submission.conf'), 'w') as fconf:
		uoj_download(submission['content']['file_name'], uoj_judger_path('/work/all.zip'))
		execute("cd %s && unzip -q all.zip && rm all.zip" % pipes.quote(uoj_judger_path('/work')))
		for k, v in submission['content']['config']:
			print(k, v, file=fconf)
		
		if 'is_hack' in submission:
			if submission['hack']['input_type'] == 'USE_FORMATTER':
				uoj_download(submission['hack']['input'], uoj_judger_path('/work/hack_input_raw.txt'))
				execute('%s <%s >%s' % (
					pipes.quote(uoj_judger_path('/run/formatter')),
					pipes.quote(uoj_judger_path('/work/hack_input_raw.txt')),
					pipes.quote(uoj_judger_path('/work/hack_input.txt'))))
			else:
				uoj_download(submission['hack']['input'], uoj_judger_path('/work/hack_input.txt'))
			print('test_new_hack_only on', file=fconf)
		elif 'is_custom_test' in submission:
			print('custom_test on', file=fconf)
	
	report_thread = Thread(target = report_loop)
	report_thread.setDaemon(True)
	
	submission_judged = False
	report_thread.start()
	execute(pipes.quote(uoj_judger_path('/main_judger')))
	submission_judged = True
	report_thread.join()
	
	return get_judger_result()

# interact with uoj web server
def uoj_interact(data, files = {}):
	data = data.copy()
	data.update({
		'judger_name': jconf['judger_name'],
		'password': jconf['judger_password']
	})
	return requests.post(uoj_url('/judge/submit'), data=data, files=files).text
def uoj_download(uri, filename):
	data = {
		'judger_name': jconf['judger_name'],
		'password': jconf['judger_password']
	}
	with open(filename, 'wb') as f:
		r = requests.post(uoj_url('/judge/download' + uri), data=data, stream=True)
		for chunk in r.iter_content(chunk_size=65536):
			if chunk:
				f.write(chunk)
def uoj_sync_judge_client():
	data = {
		'judger_name': jconf['judger_name'],
		'password': jconf['judger_password']
	}
	ret = requests.post(uoj_url('/judge/sync-judge-client'), data=data).text
	if ret != "ok":
		raise Exception('failed to sync judge clients: %s' % ret)

def send_and_fetch(result = None, fetch_new = True):
	global submission
	
	"""send judgement result, and fetch new submission to judge"""
	
	data = {}
	files = {}
	
	if not fetch_new:
		data['fetch_new'] = False
	
	if result != None:
		data['submit'] = True
		if 'is_hack' in submission:
			data['is_hack'] = True
			data['id'] = submission['hack']['id']
			if result != False and result['score']:
				try:
					print("succ hack!", file=sys.stderr)
					files = {
						('hack_input', open('uoj_judger/work/hack_input.txt', 'r')),
						('std_output', open('uoj_judger/work/std_output.txt', 'r'))
					}
				except Exception:
					print_judge_client_status()
					traceback.print_exc()
					result = False
		elif 'is_custom_test' in submission:
			data['is_custom_test'] = True
			data['id'] = submission['id']
		else:
			data['id'] = submission['id']
		
		if result == False:
			result = {
				'score': 0,
				'error': 'Judgement Failed',
				'details': 'Unknown Error'
			}
		result['status'] = 'Judged'
		data['result'] = json.dumps(result, ensure_ascii=False)
	
	while True:
		try:
			ret = uoj_interact(data, files)
			print(ret)
		except Exception:
			print_judge_client_status()
			traceback.print_exc()
		else:
			break
		time.sleep(2)
	
	try:
		submission = json.loads(ret)
	except Exception as e:
		submission = None
		return False
	else:
		return True

# judge client
def judger_loop():
	ok = False
	while True:
		fetch_new = True
		
		if ok and not (taskQ.empty() and socket_server_thread.isAlive()):
			fetch_new = False
				
		if not ok:
			while True:
				if not taskQ.empty():
					handle_task()
				if not socket_server_thread.isAlive():
					raise Exception('socket server exited unexpectedly')
				
				if send_and_fetch():
					break
				
				print('['+time.asctime()+']', 'Nothing to judge...')
				time.sleep(2)
			
			ok = True
			print_judge_client_status()
			print('judging', file=sys.stderr)
		
		try:
			res = judge()
		except Exception:
			print_judge_client_status()
			traceback.print_exc()
			res = False
		
		ok = send_and_fetch(result=res,fetch_new=fetch_new)

# main function
def main():
	init()
	
	if len(sys.argv) == 1:
		start_judger_server()
	if len(sys.argv) == 2:
		if sys.argv[1] == 'start':
			pid = os.fork()
			if pid == -1:
				raise Exception('fork failed')
			elif pid > 0:
				return
			else:
				freopen(sys.stdout, open(os.devnull, 'wb'))
				freopen(sys.stderr, open('log/judge.log', 'ab', buffering=0))
				start_judger_server()
		elif sys.argv[1] == 'update':
			try:
				with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
					s.connect(('127.0.0.1', jconf['socket_port']))
					s.sendall(json.dumps({
						'password': jconf['socket_password'],
						'cmd': 'update'
					}).encode())
				return
			except Exception:
				traceback.print_exc()
				raise Exception('update failed')
		elif sys.argv[1] == 'stop':
			try:
				with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
					s.connect(('127.0.0.1', jconf['socket_port']))
					s.sendall(json.dumps({
						'password': jconf['socket_password'],
						'cmd': 'stop'
					}).encode())
					if s.recv(10).decode() != 'ok':
						raise Exception('stop failed')
				return
			except Exception:
				traceback.print_exc()
				raise Exception('stop failed')
	raise Exception('invalid argument')

try:
	main()
except Exception:
	print_judge_client_status()
	traceback.print_exc()
	sys.exit(1)
